﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Security;
using System.Security.Cryptography.X509Certificates;
using System.Security.Policy;

namespace MSTR.IS.Net
{
    public class FTP
    {
        public const string ProtocolPrefix = @"ftp://";
        public const string AnonymousUserName = "anonymous";
        public static readonly int BufferSize = 2 * 1024 * 1024; //2MB

        public readonly string FTPRoot;
        public readonly string UserName;
        public readonly string Password;
        public readonly bool KeepAlive;
        public readonly bool UseSSL;

        public string CurrentDirectory { get; private set; }

        public FTP(string sFTPRoot, string sUserName, string sPassword, bool bKeepAlive, bool bUseSSL)
        {
            if (string.IsNullOrEmpty(sFTPRoot) || 0 == sFTPRoot.Trim().Length)
                throw new ArgumentException("Invalid FTP root.", "sHost");

            //hostname => ftp://hostname
            if (!sFTPRoot.StartsWith(ProtocolPrefix, StringComparison.InvariantCultureIgnoreCase))
                sFTPRoot = ProtocolPrefix + sFTPRoot;

            //Make sure sFTPRoot ends with "/"
            if (!sFTPRoot.EndsWith("/"))
                sFTPRoot += "/";

            FTPRoot = sFTPRoot;
            UserName = string.IsNullOrEmpty(sUserName) ? AnonymousUserName : sUserName;
            Password = sPassword ?? string.Empty;
            KeepAlive = bKeepAlive;
            UseSSL = bUseSSL;
            CurrentDirectory = string.Empty; //root
        }

        public void SetCurrentDirectory(string path)
        {
            if (null == path)
                throw new ArgumentNullException("path");

            //Must be empty or ends with "/"
            if (path.Length > 0 && !path.EndsWith("/"))
                path += "/";

            CurrentDirectory = path.TrimStart('/');
        }

        public static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors)
                return false;

            if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateNotAvailable)
                return false;

            if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateNameMismatch)
            {
                var request = sender as HttpWebRequest;
                if (null == request)
                    return false;

                var z = Zone.CreateFromUrl(request.RequestUri.ToString());
                if (z.SecurityZone == SecurityZone.Intranet || z.SecurityZone == SecurityZone.MyComputer)
                    return true;

                return false;
            }

            return true;
        }

        public FtpWebRequest CreateFTPRequest(string fileName)
        {
            var url = FTPRoot + CurrentDirectory + (fileName ?? string.Empty);
            var ftpRequest = FtpWebRequest.Create(url) as FtpWebRequest;
            ftpRequest.Credentials = new NetworkCredential(UserName, Password);
            ftpRequest.KeepAlive = KeepAlive;
            ftpRequest.EnableSsl = UseSSL;
            ftpRequest.UseBinary = true;

            //var oCert = new System.Security.Cryptography.X509Certificates.X509Certificate();
            //ftpRequest.ClientCertificates.Add(oCert);

            // Validate the server certificate with ServerCertificateValidationCallBack
            if (UseSSL)
                ServicePointManager.ServerCertificateValidationCallback = ValidateServerCertificate;

            return ftpRequest;
        }

        public List<string> GetFileList()
        {
            var ftpRequest = CreateFTPRequest(null);
            ftpRequest.Method = WebRequestMethods.Ftp.ListDirectory;

            var ftpResponse = ftpRequest.GetResponse() as FtpWebResponse;
            var list = new List<string>(50);
            using (var sr = new StreamReader(ftpResponse.GetResponseStream()))
            {
                var str = sr.ReadLine();
                while (null != str)
                {
                    list.Add(str);
                    str = sr.ReadLine();
                }
            }

            ftpResponse.Close();
            ftpRequest = null;

            return list;
        }

        public FileInfo DownloadFile(string srcFile, string saveAsFile)
        {
            var ftpRequest = CreateFTPRequest(srcFile);
            ftpRequest.Method = WebRequestMethods.Ftp.DownloadFile;

            var ftpResponse = ftpRequest.GetResponse() as FtpWebResponse;
            using (var reader = ftpResponse.GetResponseStream())
            using (var fs = new FileStream(saveAsFile, FileMode.Create))
            {
                var buffer = new byte[BufferSize]; //2MB
                var read = 1;
                while (0 != read)
                {
                    read = reader.Read(buffer, 0, buffer.Length);
                    fs.Write(buffer, 0, read);
                }

                fs.Flush();
            }

            ftpResponse.Close();
            ftpRequest = null;

            return new FileInfo(saveAsFile);
        }

        /// <summary>
        /// WebException will be thrown if file already exists.
        /// </summary>
        /// <param name="saveAsFile"></param>
        /// <param name="srcStream"></param>
        public void UploadFile(Stream srcStream, string saveAsFile)
        {
            //Test whether file already exists
            var path = Path.GetDirectoryName(saveAsFile);
            SetCurrentDirectory(CurrentDirectory + path);

            saveAsFile = Path.GetFileName(saveAsFile);
            var list = GetFileList();
            if (list.Any(li => 0 == string.Compare(li, saveAsFile, true)))
                throw new WebException("File already exists: " + CurrentDirectory + saveAsFile + ".");

            //Upload file
            var ftpRequest = CreateFTPRequest(saveAsFile);
            ftpRequest.Method = WebRequestMethods.Ftp.UploadFile;

            using (var writer = ftpRequest.GetRequestStream())
            {
                var buffer = new byte[BufferSize];
                var read = 1;
                while (0 != read)
                {
                    read = srcStream.Read(buffer, 0, buffer.Length);
                    writer.Write(buffer, 0, read);
                }

                writer.Flush();
            }
        }

        /// <summary>
        /// Delete the file and return the action result.
        /// <para>FtpStatuCode.FileActionOK will be returned if file was deleted successfully.</para>
        /// </summary>
        /// <param name="srcFile"></param>
        /// <returns></returns>
        public FtpStatusCode DeleteFile(string srcFile)
        {
            var ftpRequest = CreateFTPRequest(srcFile);
            ftpRequest.Method = WebRequestMethods.Ftp.DeleteFile;

            var ftpResponse = ftpRequest.GetResponse() as FtpWebResponse;
            ftpResponse.Close();
            ftpRequest = null;

            return ftpResponse.StatusCode;
        }
    }
}
